package com.bumptech.glide.load.engine.bitmap_recycle;

import com.bumptech.glide.util.LogUtil;
import com.bumptech.glide.util.Synthetic;
import com.bumptech.glide.util.Util;
import ohos.media.image.PixelMap;
import ohos.media.image.common.AlphaType;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.ScaleMode;
import ohos.media.image.common.Size;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * An {@link com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool} implementation that uses an
 * {@link com.bumptech.glide.load.engine.bitmap_recycle.LruPoolStrategy} to bucket {@link Bitmap}s
 * and then uses an LRU eviction policy to evict Bitmap's from the least
 * recently used bucket in order to keep the pool below a given maximum size limit.
 */
public class LruBitmapPool implements BitmapPool {
  private static final String TAG = "LruBitmapPool";
  private static final PixelFormat DEFAULT_CONFIG = PixelFormat.ARGB_8888;

  private final LruPoolStrategy strategy;
  private final Set<PixelFormat> allowedConfigs;
  private final long initialMaxSize;
  private final BitmapTracker tracker;

  private long maxSize;
  private long currentSize;
  private int hits;
  private int misses;
  private int puts;
  private int evictions;

  // Exposed for testing only.
  LruBitmapPool(long maxSize, LruPoolStrategy strategy, Set<PixelFormat> allowedConfigs) {
    this.initialMaxSize = maxSize;
    this.maxSize = maxSize;
    this.strategy = strategy;
    this.allowedConfigs = allowedConfigs;
    this.tracker = new NullBitmapTracker();
  }

  /**
   * Constructor for LruBitmapPool.
   *
   * @param maxSize The initial maximum size of the pool in bytes.
   */
  public LruBitmapPool(long maxSize) {
    this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
  }

  /**
   * Constructor for LruBitmapPool.
   *
   * @param maxSize The initial maximum size of the pool in bytes.
   * @param allowedConfigs A white listed put of Bitmap.Config that are
   *     allowed to be put into the pool. Configs not in the allowed put will be rejected.
   */
  // Public API.
  @SuppressWarnings("unused")
  public LruBitmapPool(long maxSize, Set<PixelFormat> allowedConfigs) {
    this(maxSize, getDefaultStrategy(), allowedConfigs);
  }

  /** Returns the number of cache hits for bitmaps in the pool.
   * @return long */
  public long hitCount() {
    return hits;
  }

  /** Returns the number of cache misses for bitmaps in the pool.
   * @return long*/
  public long missCount() {
    return misses;
  }

  /** Returns the number of bitmaps that have been evicted from the pool.
   * @return long */
  public long evictionCount() {
    return evictions;
  }

  /** Returns the current size of the pool in bytes.
   * @return long */
  public long getCurrentSize() {
    return currentSize;
  }

  @Override
  public long getMaxSize() {
    return maxSize;
  }

  @Override
  public synchronized void setSizeMultiplier(float sizeMultiplier) {
    maxSize = Math.round(initialMaxSize * sizeMultiplier);
    evict();
  }

  @Override
  public synchronized void put(PixelMap pixelmap) {
    if (pixelmap == null) {
      throw new NullPointerException("pixelmap must not be null");
    }
    if (pixelmap.isReleased()) {
      throw new IllegalStateException("Cannot pool recycled pixelmap");
    }

    //There is no API to check IsEditable(), right now we think pixelmap is editable
    if (strategy.getSize(pixelmap) > maxSize
        || !allowedConfigs.contains(pixelmap.getImageInfo().pixelFormat)) {
      if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
        LogUtil.info(
            TAG,
            "Reject bitmap from pool"
                + ", bitmap: "
                + strategy.logBitmap(pixelmap)
                + ", is mutable: "
                + ", is allowed config: "
                + allowedConfigs.contains(pixelmap.getImageInfo().pixelFormat));
      }
      pixelmap.release();
      return;
    }

    final int size = strategy.getSize(pixelmap);
    strategy.put(pixelmap);
    tracker.add(pixelmap);

    puts++;
    currentSize += size;

    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
      LogUtil.info(TAG, "Put bitmap in pool=" + strategy.logBitmap(pixelmap));
    }
    dump();

    evict();
  }

  private void evict() {
    trimToSize(maxSize);
  }

  @Override
  @NotNull
  public PixelMap get(int width, int height, PixelFormat config) {

    PixelMap result = getDirtyOrNull(width, height, config);
    if (result != null) {
      // Bitmaps in the pool contain random data that in some cases must be cleared for an image
      // to be rendered correctly. we shouldn't force all consumers to independently erase the
      // contents individually, so we do so here. See issue #131.
      result.writePixels(Util.COLOR_TRANS);
    } else {
      result = createPixelmap(width, height, config);
    }

    return result;
  }

  @NotNull
  @Override
  public PixelMap getDirty(int width, int height, PixelFormat config) {

    PixelMap result = getDirtyOrNull(width, height, config);
    if (result == null) {
      result = createPixelmap(width, height, config);
    }
    return result;
  }

  public static PixelMap.InitializationOptions getdefaultinitOptions(int width, int height,  PixelFormat config) {
    PixelMap.InitializationOptions opts = new PixelMap.InitializationOptions();
    opts.alphaType =AlphaType.OPAQUE;
    opts.editable = true;
    opts.pixelFormat = (config != null ? config : DEFAULT_CONFIG);
    opts.releaseSource = false;
    opts.scaleMode = ScaleMode.FIT_TARGET_SIZE;
    opts.size = new Size(width, height);
    opts.useSourceIfMatch = true;

    return opts;
  }
  
  @NotNull
  private static PixelMap createPixelmap(int width, int height,  PixelFormat config) {
    PixelMap.InitializationOptions opts = new PixelMap.InitializationOptions();
    opts.size = new Size(width, height);
    opts.pixelFormat = config;

    return PixelMap.create( opts);
  }


  private static void assertNotHardwareConfig(PixelFormat config) {
    // Right now pixelmap supports only 2 configs and no hardware config   
      return;  
  }

  @Nullable
  private synchronized PixelMap getDirtyOrNull(
      int width, int height,  PixelFormat config) {
    assertNotHardwareConfig(config);

    // Config will be null for non public config types, which can lead to transformations naively
    // passing in null as the requested config here. See issue #194.
    final PixelMap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    if (result == null) {
      if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
        LogUtil.info(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
      }
      misses++;
    } else {
      hits++;
      currentSize -= strategy.getSize(result);
      tracker.remove(result);
      normalize(result);
    }
    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
      LogUtil.info(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
    }
    dump();

    return result;
  }

  // Setting these two values provides Bitmaps that are essentially equivalent to those returned
  // from Pixelmap.createPixelmap.
  private static void normalize(PixelMap pixelmap) {
    pixelmap.setAlphaType(AlphaType.PREMUL);
    maybeSetPreMultiplied(pixelmap);
  }

  private static void maybeSetPreMultiplied(PixelMap pixelmap) {

      pixelmap.setAlphaType(AlphaType.PREMUL);
  }

  @Override
  public void clearMemory() {

    //LogUtil.info(TAG, "clearMemory");

    trimToSize(0);
  }

  @Override
  public void trimMemory(int level) {
    //TODO: , check this ComponentCallbacks2
    trimToSize(getMaxSize() / 2);
  }

  private synchronized void trimToSize(long size) {

    while (currentSize > size) {
      final PixelMap removed = strategy.removeLast();
      // TODO: This shouldn't ever happen, see #331.
      if (removed == null) {
        if (LogUtil.isLoggable(TAG, LogUtil.WARN)) {
          LogUtil.warn(TAG, "Size mismatch, resetting");
          dumpUnchecked();
        }
        currentSize = 0;
        return;
      }
      tracker.remove(removed);
      currentSize -= strategy.getSize(removed);
      evictions++;
      if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
        LogUtil.info(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
      }
      dump();
      removed.release();
    }
  }

  private void dump() {
    if (LogUtil.isLoggable(TAG, LogUtil.VERBOSE)) {
      dumpUnchecked();
    }
  }

  private void dumpUnchecked() {
    LogUtil.info(
        TAG,
        "Hits="
            + hits
            + ", misses="
            + misses
            + ", puts="
            + puts
            + ", evictions="
            + evictions
            + ", currentSize="
            + currentSize
            + ", maxSize="
            + maxSize
            + "\nStrategy="
            + strategy);
  }

  private static LruPoolStrategy getDefaultStrategy() {
      return new SizeConfigStrategy();
  }
  
  private static Set<PixelFormat> getDefaultAllowedConfigs() {

    Set<PixelFormat> configs = new HashSet<>(Arrays.asList(PixelFormat.values()));

    // GIFs, among other types, end up with a native Bitmap config that doesn't map to a java
    // config and is treated as null in java code. On KitKat+ these Bitmaps can be reconfigured
    // and are suitable for re-use.
    configs.add(null);

    configs.remove(PixelFormat.UNKNOWN); //Remove unknown format
    return Collections.unmodifiableSet(configs);
  }

  private interface BitmapTracker {
    void add(PixelMap pixelmap);

    void remove(PixelMap pixelmap);
  }

  @SuppressWarnings("unused")
  // Only used for debugging
  private static class ThrowingBitmapTracker implements BitmapTracker {
    private final Set<PixelMap> bitmaps = Collections.synchronizedSet(new HashSet<PixelMap>());

    @Override
    public void add(PixelMap pixelmap) {

      if (bitmaps.contains(pixelmap)) {
        throw new IllegalStateException(
            "Can't add already added pixelmap: "
                + pixelmap
                + " ["
                + pixelmap.getImageInfo().size.width
                + "x"
                + pixelmap.getImageInfo().size.height
                + "]");
      }
      bitmaps.add(pixelmap);
    }

    @Override
    public void remove(PixelMap pixelmap) {

      if (!bitmaps.contains(pixelmap)) {
        throw new IllegalStateException("Cannot remove bitmap not in tracker");
      }
      bitmaps.remove(pixelmap);
    }
  }

  private static final class NullBitmapTracker implements BitmapTracker {

    @Synthetic
    NullBitmapTracker() {}

    @Override
    public void add(PixelMap pixelmap) {
      // Do nothing.
    }

    @Override
    public void remove(PixelMap pixelmap) {
      // Do nothing.
    }
  }
}
